package com.example.myapplication;

import java.nio.*;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.*;
import java.lang.*;
import java.net.InetSocketAddress;

class sockData {
    boolean doR, doW, peerFull, tellMyFull, peerClose;
    byte[] data, id;

    sockData(byte[] d) {
        doR = doW = peerClose = peerFull = tellMyFull = false;
        data = new byte[0];
        id = d;
    }
}

public class connc {
    ServerSocketChannel accepSock;
    SocketChannel connSock;
    Map<String, SocketChannel> idMap = new HashMap<String, SocketChannel>();
    Map<SocketChannel, sockData> proxySocks = new HashMap<SocketChannel, sockData>();

    byte[] peerBuf, readCache;
    int peerBufMax = 200 * 1024;
    int connNum = 0;
    int maxNum = 900;
    byte[] decodeData;
    byte[] decodeId;
    int decodeType;
    int askConn_g = 1;
    int connBack_g = 2;
    int full_g = 3;
    int notfull_g = 4;
    int data_g = 5;
    int close_g = 6;
    int cachePerConn = 200 * 1024;
    logClass myLog;

    public connc(String serverip, int localPort, int increaseDose, int deDose, int pushA,
                 int sizeLi, int limit, int speed, ArrayList<Integer> lp, byte[] salt, logClass lo)
            throws Exception {
        connSock = SocketChannel.open();
        myLog = lo;
        streamc stc = new streamc(serverip, increaseDose, deDose, pushA, sizeLi, limit, speed, lp, salt, lo);

        myThr aa = new myThr();
        aa.st = stc;
        aa.setDaemon(true);
        aa.start();
        InetSocketAddress add2 = new InetSocketAddress("localhost", stc.listenPort);
        connSock.connect(add2);
        myLog.setLog("connc starts");
        InetSocketAddress add = new InetSocketAddress("0.0.0.0", localPort);
        accepSock = ServerSocketChannel.open();
        accepSock.configureBlocking(false);
        accepSock.bind(add);
        peerBuf = new byte[0];
        readCache = new byte[0];
        connSock.configureBlocking(false);
        while (true) {
            try {
                doWork();
            } catch (Exception e) {
                helpFunc.print(e.toString());
            }
        }
    }

    void rig(Selector s) throws Exception {
        for (SocketChannel sock : proxySocks.keySet()) {
            sockData d = proxySocks.get(sock);
            d.doR = d.doW = false;
            int k = 0;
            if (peerBuf.length < peerBufMax && (!d.peerFull&&(!d.peerClose))) {
                d.doR = true;
                k |= SelectionKey.OP_READ;
            }
            if (d.data.length > 0) {
                d.doW = true;
                k |= SelectionKey.OP_WRITE;
            }
            if (k != 0) {
                sock.register(s, k);
            }
        }
    }

    void dealAccep() throws Exception {

        SocketChannel sock = accepSock.accept();
        sock.configureBlocking(false);
        connNum += 1;
        helpFunc.print("ask conn");
        if (connNum > maxNum) {
            sock.close();
            connNum -= 1;
            helpFunc.print("too much conn");
        } else {
            byte[] id = helpFunc.getuuid_byte();
            sock.configureBlocking(false);
            idMap.put(helpFunc.bytesToHex(id), sock);
            byte[] msg = makeMsg(askConn_g, id, new byte[1]);
            addPeerBuf(msg);
        }
    }

    void addPeerBuf(byte[] msg) {
        byte[] t = new byte[msg.length + peerBuf.length];
        System.arraycopy(peerBuf, 0, t, 0, peerBuf.length);
        System.arraycopy(msg, 0, t, peerBuf.length, msg.length);
        peerBuf = t;
    }

    byte[] makeMsg(int type, byte[] id, byte[] data) {
        structWrapper re = new structWrapper(5 + id.length + data.length);
        re.writeDWord(5 + id.length + data.length);
        re.writeByte((byte) type);
        re.writeArr(id);
        re.writeArr(data);
        return re.data;
    }

    void decodeMsg(byte[] msg) {
        structWrapper re = new structWrapper(msg);
        decodeType = re.readByte();
        decodeId = new byte[16];
        System.arraycopy(msg, 1, decodeId, 0, 16);
        decodeData = new byte[msg.length - 17];
        System.arraycopy(msg, 17, decodeData, 0, msg.length - 17);
    }

    void dealConnSockRead() throws Exception {
        ByteBuffer bu = ByteBuffer.allocate(65535);
        int bytesRead = connSock.read(bu);
        byte[] temp = new byte[readCache.length + bytesRead];
        System.arraycopy(readCache, 0, temp, 0, readCache.length);
        System.arraycopy(bu.array(), 0, temp, readCache.length, bytesRead);
        readCache = temp;
        ArrayList<byte[]> ll = dealData();

        for (byte[] msg : ll) {
            decodeMsg(msg);
            if (decodeType == connBack_g) {
                byte ok = decodeData[0];
                SocketChannel sock = idMap.get(helpFunc.bytesToHex(decodeId));
                if (ok == 1) {
                    sockData d = new sockData(decodeId);
                    proxySocks.put(sock, d);
                } else {
                    connNum -= 1;
                    sock.close();
                    idMap.remove(helpFunc.bytesToHex(decodeId));
                }
            } else {
                dealNormalConnSock();
            }
        }
    }

    void dealNormalConnSock() {
        if (!idMap.containsKey(helpFunc.bytesToHex(decodeId))) return;
        SocketChannel sock = idMap.get(helpFunc.bytesToHex(decodeId));
        if (decodeType == full_g)
            proxySocks.get(sock).peerFull = true;
        if (decodeType == notfull_g)
            proxySocks.get(sock).peerFull = false;
        if (decodeType == data_g) {
            proxySocks.get(sock).data = helpFunc.bufAdd(proxySocks.get(sock).data, decodeData);
            if (proxySocks.get(sock).data.length > cachePerConn && (!proxySocks.get(sock).tellMyFull)) {
                byte[] msg = makeMsg(full_g, decodeId, new byte[1]);
                addPeerBuf(msg);
            }
        }
        if (decodeType == close_g) {
            proxySocks.get(sock).peerClose = true;
            if (proxySocks.get(sock).data.length == 0) {
                doClose(sock, decodeId);
            }
        }
    }

    void doClose(SocketChannel sock, byte[] id) {
        try {
            sock.close();
            connNum -= 1;
            proxySocks.remove(sock);
            idMap.remove(helpFunc.bytesToHex(id));
            helpFunc.print("close");
        } catch (Exception e) {
            helpFunc.print(e.toString());
        }
    }

    void dealProxySockR(SocketChannel sock) {
        byte[] temp;
        int bytesRead;
        ByteBuffer bu = ByteBuffer.allocate(65535);
        try {
            bytesRead = sock.read(bu);
            if (bytesRead <= 0) throw new Exception();
            temp = new byte[bytesRead];
            System.arraycopy(bu.array(), 0, temp, 0, bytesRead);
        } catch (Exception e) {
            temp = new byte[0];
        }
        byte[] id = proxySocks.get(sock).id;
        if (temp.length == 0) {
            doClose(sock, id);
            addPeerBuf(makeMsg(close_g, id, new byte[1]));
            return;
        }
        addPeerBuf(makeMsg(data_g, id, temp));
    }

    void dealProxySockW(SocketChannel sock) {
        try {
            ByteBuffer temp = ByteBuffer.wrap(proxySocks.get(sock).data);
            sock.write(temp);
            proxySocks.get(sock).data = new byte[temp.remaining()];
            temp.get(proxySocks.get(sock).data, 0, proxySocks.get(sock).data.length);
            if (proxySocks.get(sock).data.length < cachePerConn && proxySocks.get(sock).tellMyFull) {
                byte[] id = proxySocks.get(sock).id;
                proxySocks.get(sock).tellMyFull = false;
                addPeerBuf(makeMsg(notfull_g, id, new byte[1]));
            }
            if (proxySocks.get(sock).data.length == 0 && proxySocks.get(sock).peerClose) {
                byte[] id = proxySocks.get(sock).id;
                doClose(sock, id);
            }
        } catch (Exception e) {
            byte[] id = proxySocks.get(sock).id;
            addPeerBuf(makeMsg(close_g, id, new byte[1]));
            doClose(sock, id);
        }
    }

    ArrayList<byte[]> dealData() {
        ArrayList<byte[]> msgList = new ArrayList<byte[]>();
        structWrapper re = new structWrapper(readCache);

        while (true) {
            if (re.leftLen() < 4)
                break;
            int ll = re.readDWord();

            if (re.leftLen() < ll - 4) {
                re.pos -= 4;
                break;
            }
            msgList.add(re.readArr(ll - 4));
        }
        readCache = re.getLeftData();
        return msgList;
    }

    public void doWork() throws Exception {
        Selector selector = Selector.open();
        boolean peerW = false;
        if (peerBuf.length > 0) {
            peerW = true;
            connSock.register(selector, SelectionKey.OP_WRITE | SelectionKey.OP_READ);
        } else {
            connSock.register(selector, SelectionKey.OP_READ);
        }
        accepSock.register(selector, SelectionKey.OP_ACCEPT);


        rig(selector);
        int ret = selector.select(1000);
        Iterator selectedKeys = selector.selectedKeys().iterator();
        while (selectedKeys.hasNext()) {
            SelectionKey key = (SelectionKey) selectedKeys.next();
            selectedKeys.remove();
            if (!key.isValid()) {
                continue;
            }
            SelectableChannel channel = key.channel();
            if (channel == accepSock && key.isAcceptable())
                dealAccep();
            if (channel == connSock && key.isReadable())
                dealConnSockRead();

            if (proxySocks.containsKey(channel) && key.isReadable() && proxySocks.get(channel).doR)
                dealProxySockR((SocketChannel) channel);

            if (proxySocks.containsKey(channel) && key.isWritable() && proxySocks.get(channel).doW)
                dealProxySockW((SocketChannel) channel);

            if (channel == connSock && peerW && key.isWritable()) {
                ByteBuffer temp = ByteBuffer.wrap(peerBuf);
                connSock.write(temp);
                peerBuf = new byte[temp.remaining()];
                temp.get(peerBuf, 0, peerBuf.length);
            }
        }
        selector.close();
    }

    public static void main(String[] args) throws Exception {
		/*
		SocketChannel so = SocketChannel.open();
		ArrayList<Integer> listenPort = new ArrayList<Integer>();
		for (int i = 50000; i < 50100; i++)
			listenPort.add(i);
		String ss = "salt";
		streamc st = new streamc(listenPort, ss.getBytes(), helpFunc.con_minRate, helpFunc.pushAhead,
				helpFunc.packLimit);
		myThr aa = new myThr();
		aa.st = st;
		aa.setDaemon(true);
		aa.start();
		InetSocketAddress add = new InetSocketAddress("localhost", st.listenPort);
		so.connect(add);
		System.out.println("ok");
	*/

    }

}
